パターンマッチ チュートリアル (PEP-636抄訳)
概要
このPEPは、PEP 634 で導入された構造的パターン・マッチングのチュートリアルです。 PEP 622はパターン・マッチングのための構文を提案し、コミュニティと運営協議会の両方で詳細な議論が行われました。その中で、この機能をどれだけ簡単に説明(学習)できるかということがよく問題になりました。このPEPは、開発者がPythonのパターンマッチについて学ぶために使用できる種類のドキュメントを提供することで、この懸念に対処するものです。
これは、PEP 634 (パターンマッチングの技術仕様) と PEP 635 (パターンマッチングを持つ動機と理由、設計上の考慮事項) のサポート資料と考えられます。
チュートリアルというよりも、簡単な復習をしたい読者のために、付録Aを参照してください。
チュートリアル
このチュートリアルの動機付けとなる例として、あなたはテキストアドベンチャーを書くことになります。テキストアドベンチャーとは、ユーザーがテキストのコマンドを入力して架空の世界を操作し、その結果をテキストで受け取る、インタラクティブフィクションの一種です。コマンドは、剣を手に入れる、ドラゴンを攻撃する、北に行く、お店に入る、チーズを買う、などの自然言語の簡略化された形になります。
マッチングシーケンス
メインループでは、ユーザーからの入力を単語に分割する必要があります。たとえば、次のような文字列のリストがあるとします。
code: python
command = input("What are you doing next? ")
# command.split()の結果を解析する処理
次のステップは、言葉を解釈することです。ほとんどのコマンドには、アクションとオブジェクトの2つの単語があります。ですから、次のようなことをしたくなるかもしれません。
code: python
... # アクション、オブジェクトの解釈
このコードの問題点は、ユーザーが2つ以上の単語を入力したり、2つ以下の単語を入力したりした場合に、何かが欠けていることです。この問題を防ぐには、単語のリストの長さをチェックするか、上記のステートメントで発生するValueErrorを捕捉します。
代わりにマッチングステートメントを使うことができます。
code: python
match command.split():
... # アクション、オブジェクトの解釈
matchステートメントは、サブジェクト(subject:)( matchキーワードの後の値)を評価し、それをパターン(pattern)(caseの後のコード)と照合します。パターンは2つの異なることを行うことができます。
サブジェクトが特定の構造を持っているかどうかを確認します。あなたのケースでは、[action, obj]パターンは、正確に2つの要素からなる任意のシーケンスにマッチします。これをマッチング(matching)といいます。
パターン内のいくつかの名前を、対象のコンポーネント要素に結びつけます。この場合、リストに2つの要素があれば、action = subject[0]、obj = subject[1]と結合されます。
一致した場合は、caseブロック内のステートメントが、バインドされた変数とともに実行されます。一致しない場合は何も起こらず、一致した後の文が次に実行されます。
なお、代入を解くのと同じように、同義語として丸括弧((...))、角括弧([...])、あるいは単なるカンマ区切りを使うことができます。つまり、case action, obj や case (action, obj) と書いても同じ意味になります。すべての形式は、あらゆるシーケンス(リストやタプルなど)にマッチします。
複数のパターンにマッチする
ほとんどのコマンドが action/object 形式であっても、異なる長さのユーザーコマンドが必要な場合があります。例えば、lookやquitのような目的語を持たない単一の動詞を追加することができます。マッチステートメントは、複数のケースを持つことができます(その可能性もあります)。
code: python
match command.split():
... # 自動詞アクションを解釈
... # アクション、オブジェクトの解釈
match文では、上から下へとパターンをチェックしていきます。そのパターンが対象と一致しない場合は、次のパターンが試行されます。ただし、最初にマッチするパターンが見つかると、そのケースの本体が実行され、それ以降のケースはすべて無視されます。これは、if/elif/elif/...文の動作に似ています。
特定の値のマッチング
コードでは、特定のアクションを見て、そのアクションに応じて異なるロジックを条件付きで実行する必要があります(例:quit, attack, or buy)。そのためには、if/elif/elif/...の連鎖や、関数の辞書を使うこともできますが、ここではパターンマッチングを活用して、このタスクを解決します。変数の代わりに、リテラル値をパターンで使うことができます(quit、42、None など)。これにより、次のように書くことができます。
code: python
match command.split():
print("Goodbye!")
quit_game()
current_room.describe()
character.get(obj, current_room)
current_room = current_room.neighbor(direction)
# 残りのコマンドはここに
["get", obj] のようなパターンは、最初の要素が "get" である 2 要素のシーケンスのみにマッチします。また、obj = subject[1]となります。
"go"のcase でわかるように、異なるパターンで異なる変数名を使用することもできます。
リテラル値は == 演算子で比較されますが、定数の True、False、None は is 演算子で比較されます。
複数の値の照合
プレイヤーは、drop key、drop sword、drop cheese といった一連のコマンドを使って、複数のアイテムをドロップすることができます。このようなインターフェースは面倒なので、drop key sword cheeseのように、1つのコマンドで複数のアイテムをドロップできるようにしたい場合があります。この場合、コマンドに含まれる単語の数は事前にわかりませんが、割り当てで認められているのと同じように、パターンで拡張アンパッキングを使用することができます。
code: python
match command.split():
for obj in objects:
character.drop(obj, current_room)
# 残りのコマンドはこちら
これは、"drop" を最初の要素として持つすべてのシーケンスにマッチします。残りの要素はすべてリストオブジェクトに取り込まれ、オブジェクト変数にバインドされます。
この構文には、シーケンスのアンパックと同様の制限があります。つまり、パターンの中に複数のスタードネームを入れることはできません。
ワイルドカードの追加
すべてのパターンが失敗したときに、コマンドが認識されなかったことを示すエラーメッセージを表示したい場合があります。先ほど学んだ機能を使い、最後のパターンとして case [*ignored_words] と書くことができます。しかし、もっと簡単な方法があります。
code: python
match command.split():
case "quit": ... # 簡略化のためコードを省略 ... # 他のパターン
case _:
print(f"Sorry, I couldn't understand {command!r}")
ワイルドカードと呼ばれるひとつのアンダースコア(_)の特別なパターンは、常にマッチしますが、変数をバインドすることはありません。
これはシーケンスだけでなく、どんなオブジェクトにもマッチすることに注意してください。そのため、最後のパターンとして単独で使用することに意味があります(エラーを防ぐために、Pythonはそれ以前に使用することを禁止します)。
パターンの合成
ここで、例題から少し離れて、今まで使ってきたパターンがどのように作られているかを理解しましょう。パターンはお互いに入れ子にすることができ、上の例では暗黙のうちにそれを行ってきました。
これまでに見てきたパターンには次のようなものでした。
「単純な」パターン:ここでいう「単純」とは、他のパターンを含まないという意味です。
捕捉パターン(方向、アクション、オブジェクトなどの独立した名称):これらを個別に議論することはなく、他のパターンの一部として使用していました。
リテラルパターン(文字列リテラル、数値リテラル、True、False、None)
ワイルドカードパターン:ひとつのアンダースコア(_)
単純ではないパターンとして、シーケンスパターン:シーケンスパターンの各要素は、実際には他のどのパターンにもなり得ます。つまり、["first", (left, right), _, *rest] のようなパターンを書くことができます。このパターンでは、少なくとも 3 つの要素のシーケンスであるサブジェクトにマッチします。また、left=subject[1][0]、right=subject[1][1]、rest=subject[3:]となります。
ORパターン
アドベンチャーゲームの例に戻りますが、同じ結果になるパターンをいくつか用意したい場合があります。例えば、"north" と "go north" というコマンドは同等のものである必要があります。また、任意のXに対して、get X、pick up X、pick X upのエイリアスを持ちたいと思うかもしれません。
パターンの中の|記号は、それらを代替品として組み合わせます。例えば、次のように書くことができます。
code: python
match command.split():
... # その他のパターン
current_room = current_room.neighbor("north")
... # pick upアクションのコードをここに
これは ORパターンと呼ばれ、期待通りの結果が得られます。パターンは左から右に向かって試行されます。これは、複数の選択肢がマッチした場合に何がバインドされるかを知るために必要なことです。ORパターンを記述する際の重要な制限は、すべての選択肢が同じ変数をバインドすることです。つまり、[1, x] | [2, y] というパターンは、マッチが成功した後にどの変数がバインドされるかが不明確になるため、許されません。[1, x] | [2, x] はまったく問題なく、成功すれば必ずx をバインドします。
マッチしたサブパターンの取得
最初のバージョンの "go " コマンドは、["go", direction]というパターンで書かれていました。最新のバージョンでは、エイリアスを使用できますが、方向がハードコードされているため、実際には north/south/east/west. のそれぞれのパターンを用意しなければなりません。これはコードの重複につながりますが、同時に入力の検証が向上し、ユーザーが入力したコマンドが方向ではなく "go figure!" であった場合には、その分岐に入らないようになります。
そこで、次のようにすることで、両方の利点を得ることができます(簡潔にするために、"go" を含まないエイリアスバージョンは省略します)。
code: python
match command.split():
current_room = current_room.neighbor(...)
# 進む方向をどうやって知るのか?
このコードは1つの分岐で、"go"の後の単語が本当に方向であるかどうかを検証しています。しかし、プレイヤーを動かすコードは、どちらが選ばれたかを知る必要がありますが、その方法がありません。そこで必要になるのが、ORパターンのような動作をしながら、同時にバインドも行うパターンです。それを実現するのが、ASパターンです。
code: python
match command.split():
current_room = current_room.neighbor(direction)
ASパターンは、その左辺にあるパターンにマッチしますが、値を名前に結びつけることもできます。
パターンへの条件の追加
これまで説明してきたパターンは、強力なデータフィルタリングを行うことができますが、時にはブーリアン式の力を最大限に発揮したい場合もあります。例えば、current_roomから出られる可能性のある出口に基づいて、限定された方向にのみ goコマンドを許可したいとします。これを実現するには、ケースにガードを追加する必要があります。ガードは、ifキーワードの後に任意の式を続けたものです。
code: python
match command.split():
current_room = current_room.neighbor(direction)
print("Sorry, you can't go that way")
ガードはパターンの一部ではなく、case の一部です。ガードはパターンがマッチしたとき、そしてすべてのパターン変数がバインドされた後にのみチェックされます(上の例でコンディションがdirection変数を使用できるのはこのためです)。パターンがマッチし、条件が真実であれば、ケースの本体は通常通り実行されます。パターンがマッチしても条件が偽であれば、match文はパターンがマッチしなかったかのように次のケースのチェックを進めます(一部の変数がすでにバインドされているという副作用が生じる可能性があります)。
UIの追加。オブジェクトのマッチング
あなたのアドベンチャーは成功しつつあり、グラフィカルインターフェースの実装を依頼されました。UIツールキットでは、event.get() を呼び出して新しいイベントオブジェクトを取得するイベントループを書くことができます。生成されるオブジェクトは、ユーザーのアクションに応じて、異なるタイプと属性を持つことができます。
KeyPressオブジェクトは、ユーザーがキーを押したときに生成されます。KeyPressオブジェクトは、ユーザーがキーを押したときに生成されます。このオブジェクトは、押されたキーの名前を表すkey_name属性と、修飾子に関するその他の属性を持ちます。
Clickオブジェクトは、ユーザーがマウスをクリックしたときに生成されます。ポインタの座標を表すposition属性を持ちます。
Quitオブジェクトは、ユーザーがゲーム・ウィンドウのクローズ・ボタンをクリックしたときに生成されます。
複数の isinstance() チェックを記述する代わりに、パターンを使用して異なる種類のオブジェクトを認識し、その属性にもパターンを適用することができます。
code: python
match event.get():
case Click(position=(x, y)):
handle_click_at(x, y)
case KeyPress(key_name="Q") | Quit():
game.quit()
case KeyPress(key_name="up arrow"):
game.go_north()
...
case KeyPress():
pass # Ignore other keystrokes
case other_event:
raise ValueError(f"Unrecognized event: {other_event}")
Click(position=(x, y)) のようなパターンは、イベントのタイプがClickクラスのサブクラスである場合にのみマッチします。また、イベントが(x, y)パターンにマッチするposition属性を持っている必要があります。一致した場合、ローカルのxとyには期待通りの値が入ります。
KeyPress() のように引数を指定しないパターンは、KeyPressクラスのインスタンスであるあらゆるオブジェクトにマッチします。パターンで指定した属性のみがマッチし、その他の属性は無視されます。
位置指定属性のマッチング
前のセクションでは、オブジェクト照合の際に名前付き属性を照合する方法を説明しました。一部のオブジェクトでは、マッチした引数を位置によって記述すると便利な場合があります(特に、属性の数が少なく、それらが「標準」の順序を持つ場合)。使用しているクラスが名前付きのタプルやデータクラスであれば、オブジェクトを構築するときに使用するのと同じ順序に従うことで可能になります。例えば、上記のUIフレームワークがそのクラスを次のように定義しているとします。
code: python
from dataclasses import dataclass
@dataclass
class Click:
position: tuple
button: Button
上のマッチ文を次のように書き換えることができます。
code: python
match event.get():
case Click((x, y)):
handle_click_at(x, y)
(x, y)パターンは自動的にposition属性にマッチします。これは、パターンの最初の引数がデータクラス定義の最初の属性に対応しているからです。
他のクラスは属性の自然な順序を持たないため、属性と一致させるためにはパターンに明示的な名前を使用する必要があります。しかし、次の代替定義のように、位置合わせのために属性の順序を手動で指定することは可能です。
code: python
class Click:
__match_args__ = ("position", "button")
def __init__(self, position, button):
...
特殊属性 __match_args__ は、case Click((x,y)) のようなパターンで使用できる、属性の明示的な順序を定義します。
定数や列挙型に対するマッチング
上のパターンでは、すべてのマウスボタンを同じように扱っていますが、左クリックを受け入れ、他のボタンは無視したいと考えています。そうしているうちに、button属性がenum.Enumで作られた列挙体であるButtonとしてタイプされていることに気づきます。実際には、このように列挙型の値を照合することができます。
code: python
match event.get():
case Click((x, y), button=Button.LEFT): # This is a left click
handle_click_at(x, y)
case Click():
pass # 他のクリックは無視
これはドット付きの名前(math.piのような)であれば動作します。しかし、修飾されていない名前(ドットのない素の名前)は常にキャプチャパターンとして解釈されるため、パターンには常に修飾された定数を使用することで、その曖昧さを回避します。
クラウドへの移行 マッピング
あなたは自分のゲームのオンライン版を作ることにしました。すべてのロジックはサーバーに、UIはクライアントに配置し、JSONメッセージを使って通信します。jsonモジュールを介して、それらはPythonの辞書、リスト、その他の組み込みオブジェクトにマッピングされます。
クライアントは、(JSONから解析された)アクションの辞書のリストを受け取り、各要素はたとえば次のようになります。
{"text": "The shop keeper says 'Ah! We have Camembert, yes sir'", "color": "blue"}
クライアントが一時停止した場合 {"sleep": 3}
サウンドを再生する場合 {"sound": "filename.ogg", "format": "ogg"}
これまでのパターンはシーケンスを処理するものでしたが、現在のキーに基づいてマッピングをマッチさせるパターンもあります。この場合は次のようになります。
code: python
for action in actions:
match action:
case {"text": message, "color": c}:
ui.set_text_color(c)
ui.display(message)
case {"sleep": duration}:
ui.wait(duration)
case {"sound": url, "format": "ogg"}:
ui.play(url)
case {"sound": _, "format": _}:
warning("Unsupported audio format")
マッピングパターンのキーはリテラルである必要がありますが、値はどんなパターンでも構いません。シーケンスパターンと同様に、一般的なパターンがマッチするためには、すべてのサブパターンがマッチする必要があります。
マッピングパターンの中で **rest を使用すると、サブジェクト内の追加のキーをキャプチャすることができます。これを省略すると、マッチング時にサブジェクト内の追加のキーは無視されることに注意してください。{"text": "foo", "color": "red", "style": "bold"} というメッセージは、上記の例の最初のパターンにマッチします。
組み込みクラスのマッチング
上のコードには検証が必要です。メッセージが外部から送られてきたことを考えると、フィールドのタイプが間違っている可能性があり、バグやセキュリティ問題につながる可能性があります。
int の bool strやintなどの組み込みクラスも含めて,あらゆるクラスが有効な照合対象となります.そのため、上記のコードとクラスパターンを組み合わせることができます。つまり、{"text": message, "color": c} と書く代わりに、{"text": str() as message, "color": str() as c} を使えば、messageとc が両方とも文字列であることを確認できます。多くの組み込みクラス (一覧は PEP-634 を参照) では、位置パラメーターを省略形として使用することができ、str() as c ではなく str(c) と書くことができます。 code: python
for action in actions:
match action:
case {"text": str(message), "color": str(c)}:
ui.set_text_color(c)
ui.display(message)
case {"sleep": float(duration)}:
ui.wait(duration)
case {"sound": str(url), "format": "ogg"}:
ui.play(url)
case {"sound": _, "format": _}:
warning("Unsupported audio format")
付録A -- クイックイントロダクション
matchステートメントは、式を受け取り、その値を1つまたは複数のcaseブロックとして与えられた連続したパターンと比較します。これは、C、Java、JavaScript(およびその他多くの言語)のswitch文と表面的には似ていますが、はるかに強力です。
最も単純な形式では、対象となる値を1つまたは複数のリテラルと比較します。
code: python
def http_error(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the Internet"
最後のブロックに注目してください。「変数名」の_ はワイルドカードとして機能し、マッチしないことはありません。
複数のリテラルをひとつのパターンにまとめるには、|(or)を使います。
code: python
case 401 | 403 | 404:
return "Not allowed"
パターンは、割り当てを解除するように見え、変数を結合するために使用することができます。
code: python
# ポイントは (x, y) のタプル
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
それをよく研究してください。最初のパターンはリテラルが2つあり、上で示したリテラルパターンの延長線上にあると考えることができます。しかし、次の2つのパターンはリテラルと変数を組み合わせたもので、変数には主語(point)からの値が束縛されています。4つ目のパターンは2つの値を束ねるので、概念的には解答の代入(x, y) = pointと似ています。
データの構造化にクラスを使用している場合、クラス名の後にコンストラクタのような引数リストを使用することができますが、属性を変数に取り込むことができるようになっています。
code: python
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def where_is(point):
match point:
case Point(x=0, y=0):
print("Origin")
case Point(x=0, y=y):
print(f"Y={y}")
case Point(x=x, y=0):
print(f"X={x}")
case Point():
print("Somewhere else")
case _:
print("Not a point")
位置指定のパラメータは、属性の順序付けを行ういくつかの組み込みクラス(DataClassなど)で使用することができます。また、クラスに__match_args__という特別な属性を設定することで、パターン内の属性の特定の位置を定義することができます。この属性を ("x", "y") に設定すると、以下のパターンはすべて等価になります (y 属性を var 変数にバインドするのもすべて同じです)。
code: python
Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)
パターンは任意に入れ子にすることができます。例えば、短いポイントのリストがあれば、次のようにマッチさせることができます。
code: python
match points:
case []:
print("No points")
print("The origin")
print(f"Single point {x}, {y}")
print(f"Two on the Y axis at {y1}, {y2}")
case _:
print("Something else")
パターンには、「ガード」と呼ばれるif節を追加することができます。ガードが偽であれば、matchは次のcaseブロックの試行に進みます。値の取得はガードが評価される前に行われることに注意してください。
code: python
match point:
case Point(x, y) if x == y:
print(f"Y=X at {x}")
case Point(x, y):
print(f"Not on the diagonal")
他にもいくつかの重要な機能があります。
unpacking assignmentsと同様に、tupleパターンとlistパターンも全く同じ意味を持ち、実際に任意の配列にマッチします。重要な例外として、イテレータや文字列にはマッチしません。(厳密には、サブジェクトは collections.abc.Sequence のインスタンスでなければなりません。)
シーケンスパターンはワイルドカードをサポートしています。[x, y, *rest] および (x, y, *rest) は、割り当てを解除する際のワイルドカード(_)と同様の働きをします。(x, y, *_) は、少なくとも2つのアイテムのシーケンスにマッチしますが、残りのアイテムは拘束されません。
マッピングパターン。辞書{"bandwidth": b, "latency": l} から "bandwidth" と "latency" の値をキャプチャします。シーケンスパターンとは異なり、余分なキーは無視されます。ワイルドカードの **rest もサポートされています。(ただし、**_ は冗長になるため、使用できません。)
サブパターンは as キーワードを使ってキャプチャすることができます。
code: python
case (Point(x1, y1), Point(x2, y2) as p2): ...
ほとんどのリテラルは等式で比較されますが、シングルトンの True、False、None は等式で比較されます。
パターンでは、名前付きの定数を使用することができます。これらは、変数をキャプチャしたと解釈されるのを防ぐために、ドット付きの名前でなければなりません。
code: pyton
from enum import Enum
class Color(Enum):
RED = 0
GREEN = 1
BLUE = 2
match color:
case Color.RED:
print("I see red!")
case Color.GREEN:
print("Grass is green")
case Color.BLUE:
print("I'm feeling the blues :(")